package lumis.portal.dao.hibernate;

import lumis.portal.PortalException;
import lumis.portal.dao.DaoException;
import lumis.portal.transaction.ITransaction;
import lumis.portal.transaction.ITransactionObserver;
import lumis.portal.transaction.spi.ITransactionProviderCallback;
import lumis.portal.transaction.spi.ITransactionSPI;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;

import org.hibernate.Session;

/**
 * Implements a transaction object
 *
 * @version $Revision: 10753 $ $Date: 2009-07-27 20:08:07 -0300 (Mon, 27 Jul 2009) $
 * @since 4.0.0
 */
public class TransactionHibernate implements ITransactionHibernate, ITransactionSPI
{
	private static ILogger logger = LoggerFactory.getLogger(TransactionHibernate.class);
	private final List<ITransactionObserver> observers = new ArrayList<ITransactionObserver>();
	
	/**
	 * Wrapper implementation that changes the default id used by
	 * {@link #getSession()} and {@link #getConnection()}.
	 * This class is used by {@link TransactionHibernate#getByConnectionId(String)}.
	 *
	 * @since 4.0.4
	 */
	private class DefaultWrapper implements ITransactionHibernate
	{
		private String defaultId;

		public DefaultWrapper(String defaultId)
		{
			this.defaultId = defaultId;
		}
		public boolean isActive()
		{
			return TransactionHibernate.this.isActive();
		}
		public void begin() throws DaoException
		{
			TransactionHibernate.this.begin();
		}
		public void commit() throws DaoException
		{
			TransactionHibernate.this.commit();
		}
		public void dispose() throws DaoException
		{
			TransactionHibernate.this.dispose();
		}
		public Connection getConnection() throws DaoException
		{
			return TransactionHibernate.this.getConnection(defaultId);
		}
		public Connection getConnection(String databaseId) throws DaoException
		{
			return TransactionHibernate.this.getConnection(databaseId);
		}
		public ITransaction getByConnectionId(String id) throws DaoException
		{
			return TransactionHibernate.this.getByConnectionId(id);
		}
		public EntityManager getEntityManager()
		{
			return TransactionHibernate.this.getEntityManager(defaultId);
		}
		public EntityManager getEntityManager(String databaseId)
		{
			return TransactionHibernate.this.getEntityManager(databaseId);
		}
		public Session getSession() throws DaoException
		{
			return TransactionHibernate.this.getSession(defaultId);
		}
		public Session getSession(String databaseId) throws DaoException
		{
			return TransactionHibernate.this.getSession(databaseId);
		}
		public void rollback() throws DaoException
		{
			TransactionHibernate.this.rollback();
		}
		public boolean addObserver(ITransactionObserver observer) throws PortalException
		{
			return TransactionHibernate.this.addObserver(observer);
		}
		public boolean removeObserver(ITransactionObserver observer) throws PortalException
		{
			return TransactionHibernate.this.removeObserver(observer);
		}
		public Object getAttribute(String name)
		{
			return TransactionHibernate.this.getAttribute(name);
		}
		public void removeAttribute(String name)
		{
			TransactionHibernate.this.removeAttribute(name);
		}
		public void setAttribute(String name, Object value)
		{
			TransactionHibernate.this.setAttribute(name, value);
		}
	}

	/**
	 * The string that indicates the default session should be used.
	 */
	private static final String DEFAULT_SESSION_ID = "default_session_id";

	/**
	 * The entity managers currently available in this transaction.
	 * @since 4.2.0
	 */
	private Map<String, EntityManager> entityManagers = new HashMap<String, EntityManager>();

	/**
	 * Indicates if this session has finalized.
	 */
	private boolean finalized = false;

	/**
	 * Indicates if this transaction is active.
	 * @since 4.0.8
	 */
	private boolean transactionActive = false;
	
	/**
	 * Indicates if this transaction's state is in transition. Is true during
	 * begin, commit and rollback operations, and is used to prevent concurrent
	 * calls for these operations.
	 */
	private boolean transactionStateInTransition = false;
	
	/**
	 * The time of when this transaction began in milliseconds. Its value
	 * is zero if the transaction is not active.
	 */
	private long beginTimeMillis = 0;
	
	/**
	 * The attributes stored in this transaction. This map is lazily created
	 * by {@link #getAttributes()}.
	 * @since 4.1.0
	 */
	private Map<String, Object> attributes;
	
	/**
	 * Stores the transaction provider callback set by 
	 * {@link #setTransactionProviderCallback(ITransactionProviderCallback)}.
	 * @since 4.2.0
	 */
	private ITransactionProviderCallback transactionProviderCallback;
	
	/**
	 * Returns the attributes map stored in this transaction, initializing it
	 * if necessary.
	 * @return the attributes map.
	 * @since 4.1.0
	 */
	private Map<String, Object> getAttributes()
	{
		if (attributes == null)
			attributes = new HashMap<String, Object>();
		return attributes;
	}

	public EntityManager getEntityManager()
	{
		return getEntityManager(DEFAULT_SESSION_ID);
	}

	public EntityManager getEntityManager(String databaseId)
	{
		if (!transactionActive)
			throw new IllegalStateException("Entity manager being requested in an inactive transaction");

		if (databaseId == null)
			databaseId = DEFAULT_SESSION_ID;

		EntityManager entityManager = entityManagers.get(databaseId);
		if (entityManager == null)
		{
			if (DEFAULT_SESSION_ID.equals(databaseId))
				entityManager = HibernateUtil.getEntityManagerFactory().createEntityManager();
			else
				entityManager = HibernateUtil.getEntityManagerFactory(databaseId).createEntityManager();
			entityManager.getTransaction().begin();
			entityManagers.put(databaseId, entityManager);
		}
		return entityManager;
	}
	
	public Session getSession() throws DaoException
	{
		return (Session)getEntityManager().getDelegate();
	}

	public Session getSession(String databaseId) throws DaoException
	{
		return (Session)getEntityManager(databaseId).getDelegate();
	}

	public void dispose() throws DaoException
	{
		try
		{
			this.finalize();
		}
		catch (Throwable e)
		{
			throw new RuntimeException(e);
		}
	}

	protected void finalize() throws Throwable
	{
		if (finalized)
			return;

		if (transactionProviderCallback != null)
			transactionProviderCallback.transactionDisposed(this);
		
		if (transactionActive)
			rollbackInternal();

		finalized = true;
		logger.debug("transaction finalized");
	}
	
	public boolean isActive()
	{
		return transactionActive;
	}

	/**
	 * Begins a state transition in this transaction. Must end the transition
	 * with {@link #endStateTransition()}.
	 * @since 4.0.8
	 */
	private void beginStateTransition() throws DaoException
	{
		if (transactionStateInTransition)
			throw new DaoException("STR_TRANSACTION_STATE_IN_TRANSITION");
		transactionStateInTransition = true;
	}
	
	/**
	 * Ends a state transition in this transaction. The transition must
	 * have began with {@link #beginStateTransition()}.
	 * @since 4.0.8
	 */
	private void endStateTransition()
	{
		transactionStateInTransition = false;
	}
	
	public void begin() throws DaoException
	{
		beginStateTransition();
		try
		{
			if (finalized)
				throw new DaoException("STR_TRANSACTION_WAS_FINALIZED");
			if (transactionActive)
				throw new DaoException("STR_TRANSACTION_ALREADY_BEGAN");
			transactionActive = true;
			beginTimeMillis = System.currentTimeMillis();
			logger.debug("transaction began");
		}
		finally
		{
			endStateTransition();
		}
	}

	public void commit() throws DaoException
	{
		beginStateTransition();
		try
		{
			if (finalized)
				throw new DaoException("STR_TRANSACTION_WAS_FINALIZED");
			if (!transactionActive)
				throw new DaoException("STR_TRANSACTION_MUST_BEGIN_BEFORE_COMMIT");
	
			// notify observers
			logger.debug("notifying beforeCommmit to transaction observers");
			try
			{
				for (int i = 0; i < observers.size(); i++)
				{
					ITransactionObserver observer = observers.get(i);
					if (!observer.beforeCommit())
						throw new PortalException("TRANSACTION_OBSERVER_BEFORECOMMIT_HAS_FAILED");
				}
			}
			catch (Exception e)
			{
				throw new DaoException("STR_ERROR_NOTIFYING_BEFORECOMMIT_TO_TRANSACTION_OBSERVERS", e);
			}
			
			// TODO: implement a hibernate JTA transaction that extends this class and uses jta commit
			for (EntityManager entityManager: entityManagers.values())
			{
				try
				{
					entityManager.getTransaction().commit();
					entityManager.close();
				}
				catch (Exception e)
				{
					throw new DaoException("STR_EXCEPTION_DURING_COMMIT", e);
				}
			}
	
			entityManagers.clear();
			transactionActive = false;
			beginTimeMillis = 0;
			logger.debug("transaction commited");
	
			// will try to notify the observers, but whatever is the result,
			// must clear observers afterwards, since this transaction is
			// considered complete and no longer active
			try
			{
				// notify observers
				logger.debug("notifying afterCommmit to transaction observers");
				boolean observerThrewException = false;
				for (ITransactionObserver observer : observers)
				{
					try
					{
						observer.afterCommit();
					}
					catch (Exception e)
					{
						observerThrewException = true;
						logger.error("Transaction observer threw exception", e);
					}
				}
				if (observerThrewException)
					throw new DaoException("STR_ERROR_NOTIFYING_AFTERCOMMIT_TO_TRANSACTION_OBSERVERS");
			}
			finally
			{
				// clear observers
				observers.clear();
			}
		}
		finally
		{
			endStateTransition();
		}
	}

	private void rollbackInternal() throws DaoException
	{
		beginStateTransition();
		try
		{
			boolean observerThrewException = false;
			
			// notify observers
			logger.debug("notifying beforeRollback to transaction observers");
			for (int i = observers.size() - 1; i >= 0; i--)
			{
				try
				{
					observers.get(i).beforeRollback();
				}
				catch (Exception e)
				{
					observerThrewException = true;
					logger.error("Transaction observer threw exception", e);
				}
			}
			
			for (EntityManager entityManager: entityManagers.values())
			{
				try
				{
					if (entityManager.getTransaction().isActive())
						entityManager.getTransaction().rollback();
					entityManager.close();
				}
				catch (Exception e)
				{
					logger.error("Error closing hibernate session", e);
				}
			}
			entityManagers.clear();
			transactionActive = false;
			beginTimeMillis = 0;
			logger.debug("transaction rolled back");
			
			// will try to notify the observers, but whatever is the result,
			// must clear observers afterwards, since this transaction is
			// considered complete and no longer active
			try
			{
				// notify observers
				logger.debug("notifying afterRollback to transaction observers");
				for (int i = observers.size() - 1; i >= 0; i--)
				{
					try
					{
						observers.get(i).afterRollback();
					}
					catch (Exception e)
					{
						observerThrewException = true;
						logger.error("Transaction observer threw exception", e);
					}
				}
				
				if (observerThrewException)
					throw new DaoException("STR_TRANSACTION_OBSERVER_THREW_EXCEPTION_DURING_ROLLBACK");
			}
			finally
			{
				// clear observers
				observers.clear();
			}
		}
		finally
		{
			endStateTransition();
		}
	}
	
	public void rollback() throws DaoException
	{
		if (finalized)
			throw new DaoException("STR_TRANSACTION_WAS_FINALIZED");
		if (!transactionActive)
		{
			logger.warn("Rollback called without active transaction");
			return;
		}

		// TODO: implement a hibernate JTA transaction that extends this class and uses jta commit
		rollbackInternal();
	}

	public Connection getConnection() throws DaoException
	{
		return getConnection(DEFAULT_SESSION_ID);
	}

	public Connection getConnection(String databaseId) throws DaoException
	{
		if (finalized)
			throw new DaoException("STR_TRANSACTION_WAS_FINALIZED");
		if (!transactionActive)
			throw new DaoException("STR_CONNECTION_REQUESTED_WITHOUT_TRANSACTION");

		Session session = getSession(databaseId);
		
		// return session's connection
		try
		{
			return session.connection();
		}
		catch (IllegalArgumentException e)
		{
			// remove context class loader while calling Session.connection()
			// due to Hibernate's bug HHH-2281
			ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
			if (contextClassLoader != null)
				Thread.currentThread().setContextClassLoader(null);
			try
			{
				return session.connection();
			}
			finally
			{
				if (contextClassLoader != null)
					Thread.currentThread().setContextClassLoader(contextClassLoader);
			}
		}
	}

	public ITransaction getByConnectionId(String id) throws DaoException
	{
		if (id == null)
			return this;
		else
			return new DefaultWrapper(id);
	}

	public boolean addObserver(ITransactionObserver observer) throws PortalException
	{
		if (observer == null)
			throw new IllegalArgumentException("Cannot add a null observer.");
		
		synchronized (observers)
		{
			if (!observers.contains(observer))
			{
				observers.add(observer);
				return true;
			}
			else
			{
				return false;
			}
		}
	}
	
	public boolean removeObserver(ITransactionObserver observer) throws PortalException
	{
		if (observer == null)
			throw new IllegalArgumentException("'observer' argument must not be null");
		
		synchronized (observers)
		{
			return observers.remove(observer);
		}
	}

	/**
	 * Returns the time when this transaction began in milliseconds since January 1, 1970 UTC.
	 * @return the time when this transaction began in milliseconds since January 1, 1970 UTC;
	 * or zero if this transaction is not active.
	 * @since 4.1.0
	 */
	public long getBeginTimeMillis()
	{
		return beginTimeMillis;
	}

	public Object getAttribute(String name)
	{
		if (name == null)
			throw new IllegalArgumentException("'name' argument cannot be null");
		
		return getAttributes().get(name);
	}

	public void setAttribute(String name, Object value)
	{
		if (name == null)
			throw new IllegalArgumentException("'name' argument cannot be null");
		
		if (value == null)
			removeAttribute(name);
		else
			getAttributes().put(name, value);
	}

	public void removeAttribute(String name)
	{
		if (name == null)
			throw new IllegalArgumentException("'name' argument cannot be null");
		
		// if attributes is initialized, remove the attribute
		if (attributes != null)
			attributes.remove(name);
	}
	
	// Transaction SPI methods

	public void setTransactionProviderCallback(ITransactionProviderCallback callback)
	{
		transactionProviderCallback = callback;
	}
}